package cn.zf233.xcloud.web;

import cn.zf233.xcloud.commom.Const;
import cn.zf233.xcloud.commom.R;
import cn.zf233.xcloud.commom.enums.ClassifyENUM;
import cn.zf233.xcloud.commom.enums.DataUnitENUM;
import cn.zf233.xcloud.entity.File;
import cn.zf233.xcloud.entity.User;
import cn.zf233.xcloud.exception.UserDetailUpdateException;
import cn.zf233.xcloud.service.AdminService;
import cn.zf233.xcloud.service.FileService;
import cn.zf233.xcloud.service.UserService;
import cn.zf233.xcloud.service.core.MemoryCoordinate;
import cn.zf233.xcloud.service.core.PersistenceService;
import cn.zf233.xcloud.util.*;
import cn.zf233.xcloud.vo.UserVo;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.PartETag;
import org.apache.commons.lang.StringUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by zf233 on 2020/11/27
 * 文件请求控制类
 */
@RestController
@RequestMapping("/file")
public class FileController {

    @Resource
    private FileService fileService;

    @Resource
    private UserService userService;

    @Resource
    private AdminService adminService;

    @Resource
    private PersistenceService persistenceService;

    @Resource
    private ThreadPoolTaskExecutor taskExecutor;

    @Resource
    private OSSUtil ossUtil;

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private QRCodeUtil qrCodeUtil;

    @Resource
    private MemoryCoordinate memory;

    @PostMapping("/list")
    public R list(HttpSession session,
                  @RequestParam(required = false) Long parentid,
                  @RequestParam(required = false) Integer sortFlag,
                  @RequestParam(required = false) Integer sortType) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorIllegalArgument("登陆失效，等重新登录");
        }
        if (parentid == null) {
            parentid = (Long) session.getAttribute(Const.PARENTID);
        }
        session.setAttribute(Const.PARENTID, parentid);
        return fileService.list(CommonUtil.assembleUser(currentUser), parentid, sortFlag, sortType);
    }

    @PostMapping("/search")
    public R search(HttpSession session, String matchCode) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorIllegalArgument("登陆失效，等重新登录");

        }
        return fileService.search(CommonUtil.assembleUser(currentUser), matchCode);
    }

    @PostMapping("/recycle/list")
    public R listRecycle(HttpSession session) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorIllegalArgument("登陆失效，等重新登录");
        }
        return fileService.listRecycle(CommonUtil.assembleUser(currentUser));
    }

    @PostMapping("/classify")
    public R classify(HttpSession session, Integer classify) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorIllegalArgument("登陆失效，等重新登录");
        }
        return fileService.classify(CommonUtil.assembleUser(currentUser), classify);
    }

    @PostMapping(value = "/upload")
    public R upload(HttpSession session, MultipartFile[] myFile) {
        Long parentId = (Long) session.getAttribute(Const.PARENTID);
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        long uploadFileSizeTotal = Arrays.stream(myFile).mapToLong(MultipartFile::getSize).sum();
        adminService.uploadFlow(uploadFileSizeTotal);
        if (currentUser.getUseCapacityNum() + uploadFileSizeTotal > currentUser.getCapacityNum()) {
            return R.createByErrorMessage("上传失败(空间已满)");
        }
        User userInfo = CommonUtil.assembleUser(currentUser);
        R<List<File>> upload = uploadFilePreprocessing(myFile, userInfo);
        if (upload.isSuccess()) {
            try {
                return fileService.save(userInfo, upload.getData(), parentId);
            } catch (IOException e) {
                return R.createByErrorMessage("上传失败");
            }
        }
        return upload;
    }

    // 文件彻底删除
    @PostMapping(value = "/remove")
    public R delete(HttpSession session, Long[] fileid) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        R response = fileService.remove(fileid, CommonUtil.assembleUser(currentUser));
        if (!response.isSuccess()) {
            session.setAttribute(Const.PARENTID, -1);
        }
        return response;
    }

    // 放置废纸篓
    @PostMapping(value = "/recycle")
    public R recycle(HttpSession session, Long[] fileid) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        R response = fileService.recycle(fileid, CommonUtil.assembleUser(currentUser));
        if (!response.isSuccess()) {
            session.setAttribute(Const.PARENTID, -1);
        }
        return response;
    }

    @PostMapping(value = "/restore")
    public R restore(HttpSession session, Long[] fileid) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        R response = fileService.restore(fileid, CommonUtil.assembleUser(currentUser));
        if (!response.isSuccess()) {
            session.setAttribute(Const.PARENTID, -1);
        }
        return response;
    }

    @PostMapping(value = "/recycle/flush")
    public R flushRecycle(HttpSession session) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        R response = fileService.flushRecycle(CommonUtil.assembleUser(currentUser));
        if (!response.isSuccess()) {
            session.setAttribute(Const.PARENTID, -1);
        }
        return response;
    }

    @PostMapping(value = "/detail")
    public R detail(Long fileid) {
        return fileService.getDetail(fileid);
    }

    // 文件下载
    @PostMapping(value = "/download")
    public R download(HttpSession session, Long fileid) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        return fileService.download(CommonUtil.assembleUser(currentUser), fileid);
    }

    // 新建文件夹
    @PostMapping(value = "/createfolder")
    public R newFolder(HttpSession session, String foldername) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        Long parentId = (Long) session.getAttribute(Const.PARENTID);
        return fileService.newFolder(CommonUtil.assembleUser(currentUser), foldername, parentId);
    }

    // 文件重命名
    @PostMapping(value = "/rename")
    public R rename(HttpSession session, Long fileid, String newFileName) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        return fileService.rename(CommonUtil.assembleUser(currentUser), fileid, newFileName);
    }

    @PostMapping(value = "/list/lower")
    public R getLowerFolder(HttpSession session,
                            @RequestParam(defaultValue = "-1") Long parentId) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser != null) {
            return fileService.listSubFolder(CommonUtil.assembleUser(currentUser), parentId);
        }
        return R.createByErrorNeedLogin("登陆失效，请重新登录");
    }

    @PostMapping(value = "/move")
    public R move(HttpSession session,
                  Long[] fileId,
                  @RequestParam(defaultValue = "-1") Long parentId) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser != null) {
            return fileService.move(CommonUtil.assembleUser(currentUser), fileId, parentId);
        }
        return R.createByErrorNeedLogin("登陆失效，请重新登录");
    }

    @PostMapping(value = "/copy")
    public R copy(HttpSession session,
                  Long[] fileId,
                  @RequestParam(defaultValue = "-1") Long parentId) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser != null) {
            return fileService.copy(CommonUtil.assembleUser(currentUser), fileId, parentId);
        }
        return R.createByErrorNeedLogin("登陆失效，请重新登录");
    }

    @PostMapping(value = "/archive")
    public R archive(HttpSession session,
                     @RequestParam(required = false) MultipartFile qr,
                     @RequestParam(required = false) String url,
                     @RequestParam(defaultValue = "-1") Long parentId,
                     Integer type) {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        if (!(type == 0 || type == 1)) {
            return R.createByErrorMessage("分享链接或分享二维码解析有误");
        }
        if (type == 0) {
            if (qr == null || qr.getSize() == 0 || qr.getSize() > DataUnitENUM.MB.bytes()) {
                return R.createByErrorIllegalArgument("请选择1MB内的二维码");
            }
            try {
                url = qrCodeUtil.decodeQRCodeStreamForStream(qr.getInputStream());
                if (StringUtils.isBlank(url)) {
                    return R.createByErrorMessage("分享二维码解析有误");
                }
            } catch (IOException e) {
                e.printStackTrace();
                return R.createByErrorMessage("分享二维码解析有误");
            }
        }
        String regex = "^(?:(?:https?|ftp|file)://|www\\.|ftp\\.)(?:\\([-A-Za-z0-9+&@#/%=~_|$?!:,.]*\\)|[-A-Za-z0-9+&@#/%=~_|$?!:,.])*(?:\\([-A-Za-z0-9+&@#/%=~_|$?!:,.]*\\)|[A-Za-z0-9+&@#/%=~_|$])";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(url);
        if (matcher.find()) {
            return fileService.linkArchived(CommonUtil.assembleUser(currentUser), parentId, url, type);
        }
        return R.createByErrorMessage("分享链接或分享二维码解析格式有误");
    }

    // 更新用户头像 Ajax
    @PostMapping(value = "/update/head")
    public R updateHead(HttpSession session, MultipartFile headPhoto) throws UserDetailUpdateException {
        UserVo currentUser = (UserVo) session.getAttribute(Const.CURRENT_USER);
        if (currentUser == null) {
            return R.createByErrorNeedLogin("登陆失效，请重新登录");
        }
        String fileName = headPhoto.getOriginalFilename();
        if (StringUtils.isBlank(fileName)) {
            return R.createByErrorMessage("头像更新失败");
        }
        fileName = fileName.replace(" ", "");
        if (!checkUploadFileName(fileName)) {
            return R.createByErrorMessage("文件命名格式有误");
        }
        if (!ClassifyENUM.PHOTO.getCode().equals(getClassify(fileName.substring(fileName.lastIndexOf("."))))) {
            return R.createByErrorMessage("头像为图片类型");
        }
        if (headPhoto.getSize() > DataUnitENUM.MB.bytes()) {
            return R.createByErrorMessage("请选择1MB内的头像");
        }
        try {
            String name = CommonUtil.writeHeadPhoto(headPhoto.getInputStream(), currentUser.getId(), fileName);
            if (StringUtils.isBlank(name)) {
                return R.createByErrorMessage("头像更新失败");
            }
            R<UserVo> response = userService.updateHeadUrl(CommonUtil.assembleUser(currentUser), Const.USER_HEAD_URL_PREFIX + name);
            if (response.isSuccess()) {
                session.setAttribute(Const.CURRENT_USER, response.getData());
                return R.createBySuccess(response.getMsg(), response.getData().getHeadUrl());
            }
            return R.createByErrorMessage("头像更新失败");
        } catch (IOException e) {
            e.printStackTrace();
            return R.createByErrorMessage("头像更新失败");
        }
    }

    // 协调文件上传，文件先分片缓存到Redis，而后异步持久化至OSS
    private R<List<File>> uploadFilePreprocessing(MultipartFile[] myFile, User user) {

        // 合法的文件列表
        List<File> legalRecordList = new ArrayList<>();
        Queue<PersistenceService.Product> legalUploadFileQueue = new LinkedList<>();
        for (MultipartFile multipartFile : myFile) {

            // 过滤非法文件
            String fileName = multipartFile.getOriginalFilename();
            if (StringUtils.isBlank(fileName)) {
                continue;
            }
            fileName = fileName.replace(" ", "_");
            if (!checkUploadFileName(fileName)) {
                continue;
            }

            // 组装文件上传实体
            long fileSize = multipartFile.getSize();
            String fileType = multipartFile.getContentType();
            File file = assembleUploadFile(user, fileName, fileSize, fileType);

            // 锁内存
            long before = ThreadSafeDateUtil.now();
            boolean spinFlag = false;
            for (; ; ) {
                long left = memory.left();
                if (memory.use(left, fileSize)) {
                    break;
                } else {
                    long after = ThreadSafeDateUtil.now();
                    if (after - before > 500L) {
                        spinFlag = true;
                        break;
                    }
                }
            }

            // 内存锁定失败同步上传
            if (spinFlag) {
                boolean flag = this.preprocessingTimeoutDisposal(multipartFile, file);
                if (flag) {
                    legalRecordList.add(file);
                }
                continue;
            }

            // 内存锁定成功
            FileInputStream in = null;
            FileChannel channel = null;
            String fileCacheKey = Const.XCLOUD_FILE_CACHE_PREFIX + UUID.randomUUID();
            try {
                PersistenceService.Product uploadFile = new PersistenceService.Product(file, fileCacheKey);
                if (fileSize <= Const.SHARD_FILE_SIZE) {
                    redisUtil.setFileCache(fileCacheKey, multipartFile.getBytes());
                } else {
                    in = (FileInputStream) multipartFile.getInputStream();
                    channel = in.getChannel();
                    int shardSize = (int) (fileSize / Const.SHARD_FILE_SIZE + (fileSize % Const.SHARD_FILE_SIZE > 0 ? 1 : 0));
                    List<String> shardKeyList = new ArrayList<>(shardSize);
                    int count;
                    int index = 0;
                    ByteBuffer buffer;
                    if (shardSize > 64) {
                        buffer = ByteBuffer.allocateDirect(Const.SHARD_FILE_SIZE);
                    } else {
                        buffer = ByteBuffer.allocate(Const.SHARD_FILE_SIZE);
                    }
                    while ((count = channel.read(buffer)) != -1) {
                        buffer.flip();
                        byte[] shard = new byte[count];
                        buffer.get(shard);
                        String shardKey = Const.XCLOUD_FILE_SHARD_CACHE_PREFIX + index++;
                        redisUtil.setFileCache(fileCacheKey, shardKey, shard);
                        buffer.clear();
                        shardKeyList.add(shardKey);
                    }

                    /*long aaa = ThreadSafeDateUtil.now();
                    System.out.println("测试IO读取,开始读取,当前时间: " + aaa);
                    byte[] shard = new byte[Const.SHARD_FILE_SIZE];
                    while ((count = in.read(shard, 0, Const.SHARD_FILE_SIZE)) != -1) {
                        String shardKey = Const.XCLOUD_FILE_SHARD_CACHE_PREFIX + index++;
                        if (count != Const.SHARD_FILE_SIZE) {
                            byte[] realShard = new byte[count];
                            System.arraycopy(shard, 0, realShard, 0, count);
                            redisUtil.setFileCache(fileCacheKey, shardKey, realShard);
                        } else {
                            redisUtil.setFileCache(fileCacheKey, shardKey, shard);
                        }
                        shardKeyList.add(shardKey);
                    }
                    long bbb = ThreadSafeDateUtil.now();
                    System.out.println("测试IO读取,读取完成,当前时间: " + bbb);
                    System.out.println("测试IO读取,读取完成,用时:" + (bbb - aaa));*/
                    uploadFile.setShardKeyList(shardKeyList);
                }
                legalRecordList.add(file);
                legalUploadFileQueue.offer(uploadFile);
            } catch (Exception e) {
                e.printStackTrace();
                if (redisUtil.exists(fileCacheKey)) {
                    redisUtil.remove(fileCacheKey);
                }
            } finally {
                if (channel != null) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException ioException) {
                        ioException.printStackTrace();
                    }
                }
            }
        }

        // 文件名合法的文件列表不为空
        if (!legalUploadFileQueue.isEmpty()) {
            taskExecutor.execute(() -> persistenceService.production(legalUploadFileQueue));
        }
        if (legalRecordList.isEmpty()) {
            return R.createByErrorMessage("移除命名不规范的文件后,上传列表为空。未成功上传任何文件!");
        }
        return R.createBySuccess(legalRecordList);
    }

    private boolean preprocessingTimeoutDisposal(MultipartFile upload, File file) {
        OSS ossClient = ossUtil.getOSSClient();
        String randomName = file.getRandomFileName();
        if (upload.getSize() <= Const.SHARD_FILE_SIZE) {
            try {
                ossUtil.upload(ossClient, randomName, file.getOldFileName(), upload.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
                ossClient.shutdown();
                return false;
            }
        } else {
            FileInputStream in = null;
            FileChannel channel = null;
            String uploadId = null;
            try {
                in = (FileInputStream) upload.getInputStream();
                channel = in.getChannel();
                uploadId = ossUtil.initShard(ossClient, randomName, file.getOldFileName(), upload.getSize());
                int count;
                int index = 0;
                int shardSize = (int) (upload.getSize() / Const.SHARD_FILE_SIZE + (upload.getSize() % Const.SHARD_FILE_SIZE > 0 ? 1 : 0));
                List<PartETag> partETagList = new ArrayList<>(shardSize);
                ByteBuffer buffer;
                if (shardSize > 64) {
                    buffer = ByteBuffer.allocateDirect(Const.SHARD_FILE_SIZE);
                } else {
                    buffer = ByteBuffer.allocate(Const.SHARD_FILE_SIZE);
                }
                while ((count = channel.read(buffer)) != -1) {
                    PartETag partETag;
                    buffer.flip();
                    byte[] shard = new byte[count];
                    buffer.get(shard);
                    partETag = ossUtil.uploadShard(ossClient, randomName, uploadId, shard, ++index);
                    buffer.clear();
                    partETagList.add(partETag);
                }
                
                /*while ((count = in.read(shard, 0, Const.SHARD_FILE_SIZE)) != -1) {
                    PartETag partETag;
                    if (count != Const.SHARD_FILE_SIZE) {
                        byte[] realShard = new byte[count];
                        System.arraycopy(shard, 0, realShard, 0, count);
                        partETag = ossUtil.uploadShard(ossClient, randomName, uploadId, realShard, ++index);
                    } else {
                        partETag = ossUtil.uploadShard(ossClient, randomName, uploadId, shard, ++index);
                    }
                    partETagList.add(partETag);
                }*/
                ossUtil.submitShard(ossClient, randomName, uploadId, partETagList);
            } catch (Exception e) {
                e.printStackTrace();
                if (StringUtils.isNotBlank(uploadId)) {
                    ossUtil.cancelShard(ossClient, randomName, uploadId);
                }
                return false;
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException ioException) {
                        ioException.printStackTrace();
                    }
                }
                if (channel != null) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                ossClient.shutdown();
            }
        }
        return true;
    }

    // 检查上传文件名是否合法
    private Boolean checkUploadFileName(String fileName) {
        final String format = "[^\\u4E00-\\u9FA5\\uF900-\\uFA2D\\w-_.,()（）《》]";
        Pattern pattern = Pattern.compile(format);
        Matcher matcher = pattern.matcher(fileName);
        return !matcher.find() && fileName.contains(".");
    }

    // 组装上传文件对象
    private File assembleUploadFile(User user, String fileName, long fileSize, String fileType) {
        File file = new File();
        file.setUserId(user.getId());
        file.setShareId(-1L);
        file.setFolder(0);
        file.setRandomFileName(ossUtil.getRandomFileName(fileName));
        file.setOldFileName(fileName);
        file.setFileSize(fileSize);
        file.setFileType(fileType);
        file.setClassify(getClassify(fileName));
        file.setDeleteFlag(0);
        return file;
    }

    private Integer getClassify(String fileName) {
        String type = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        ClassifyENUM classify;
        return (classify = ClassifyENUM.checkClassify(type)) == null ? -1 : classify.getCode();
    }
}